# Session—1 Simple
基于 Session 的认证机制由 Servlet 规范定制,Servlet 容器已实现,用户通过操作 HttpSession 的方法即可实现,如下是HttpSession 相关的操作API。
HttpSession getSession(Boolean create);// 获取当前HttpSession对象
void setAttribute(String name,Object value);// 向session中存放对象
object getAttribute(String name);// 从session中获取对象
void removeAttribute(String name);// 移除session中对象
void invalidate();// 使HttpSession失效
// ......
# 创建工程
使用纯 JavaConfig 创建,Maven 及其他详见代码
# WebAppInitializer 配置
/**
* 相当于 /webapp/WEB-INF/web.xml,Spring 容器启动时会加载 WebApplicationInitializer 接口的所有实现类
*
* DispatcherServlet是Spring MVC的核心。在这里请求会第一次接触到框架,它主要负责将请求路由到其他的组件之中。
* 按照传统的方式,像DispatcherServlet这样的Servlet会配置在web.xml文件中,这个文件会放到应用的WAR包里面。当然,这是配置DispatcherServlet的方法之一。
* 但是,借助于Servlet3规范和Spring3.1的功能增强,这种方式已经不是唯一的方案了,这也不是我们本文所使用的配置方法。
* 我们会使用java将DispatcherServlet配置在Servlet容器中,而不会再使用web.xml文件。如下程序清单展示了所需的java类。
*
* Spring Web应用中通常存在两个应用上下文,一个是DispatcherServlet创建的应用上下文,一个是ContextLoaderListener创建的应用上下文。
* AbstractAnnotationConfigDispatcherServletInitializer会同时创建DispatcherServlet和ContextLoaderListener(查看源码)
*/
public class WebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
/**
* 返回的带有@Configuration注解的类将会用来配置ContextLoaderListener创建的应用上下文中的bean,
* 通常是驱动应用后端的中间层和数据库组件。
*/
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class[]{RootConfig.class};
}
/**
* 返回的带有@Configuration注解的类会用来定义DispatcherServlet应用上下文中的bean。
* 包含Web组件的bean,如控制器、试图解析器以及处理器映射器等。
*/
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class[]{WebConfig.class};
}
/**
* 将DispatcherServlet映射到"/"
*/
@Override
protected String[] getServletMappings() {
return new String[]{"/"};
}
}
# RootConfig 配置
/**
* 相当于 Spring 及其整合的如 Mybatis 等配置
*/
@Configuration
@ComponentScan(
value = "top.conanan.security",
excludeFilters = {
// 需排除 Controller 注解(也会自动包含 RestController)
@ComponentScan.Filter(type = FilterType.ANNOTATION, value = Controller.class)
})
public class RootConfig {
}
# WebConfig 配置
/**
* 相当于 SpringMVC 的配置
* <p>
* 我们有多种方法来配置DispatcherServlet,与之类似,启用Spring MVC组件的方法也不只一种。
* 从前,Spring是使用XML进行配置的,可以使用<mvc:annotation-driven>启用注解驱动的Spring MVC。
*/
@Configuration
@EnableWebMvc// 启用 Spring MVC 注解驱动,替代处理器映射器、处理器适配器。
@ComponentScan(
value = "top.conanan.security.controller",
includeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, value = Controller.class),
useDefaultFilters = false// 必须有!这里仅仅扫描 Controller
)
// extends WebMvcConfigurerAdapter 在 Spring 5.0 已经 Deprecated
public class WebConfig implements WebMvcConfigurer {
@Autowired
private SimpleAuthenticationInterceptor simpleAuthenticationInterceptor;
/**
* 配置视图解析器
*
* @param registry
*/
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.jsp("/WEB-INF/views/", ".jsp");
}
/**
* 配置视图控制器
*
* @param registry
*/
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("login");
}
}
# 实现认证功能 🔥
# 认证页面
认证页面就不再赘述了
<%@ page contentType="text/html;charset=utf-8" pageEncoding="utf-8" %>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>用户登录</title>
</head>
<body>
<form action="login" method="post">
用户名:<input type="text" name="username"><br>
密 码: <input type="password" name="password"><br>
<input type="submit" value="登录">
</form>
</body>
</html>
# login 页面的控制器配置
见 WebConfig 中,直接拦截到 login 页面
/**
* 配置视图控制器
*
* @param registry
*/
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("login");
}
# 认证接口!!!🔥
用户进入认证页面,输入账号和密码,点击登录,请求/login进行身份认证。
定义认证接口,此接口用于对传来的用户名、密码校验,若成功则返回该用户的详细信息,否则抛出错误异常
/** * 认证服务接口 */ public interface AuthenticationService { /** * 用户认证 * @param authenticationRequest 用户认证请求(包括账号和密码等) * @return 认证成功的用户信息 */ UserDto authentication(AuthenticationRequest authenticationRequest); }认证请求结构
/** * 用户认证请求参数 */ @Data public class AuthenticationRequest { private String username; private String password; }认证成功后返回的用户详细信息,也就是当前登录用户的信息
/** * 用户身份信息 */ @Data @NoArgsConstructor @AllArgsConstructor public class UserDto { public static final String SESSION_USER_KEY = "_user"; private String id; private String username; private String password; private String fullName; private String mobile; }认证实现类,根据用户名查找用户信息,并校验密码,这里模拟了两个用户
@Service public class AuthenticationServiceImpl implements AuthenticationService { /** * 用户认证,校验用户信息是否合法 * @param authenticationRequest 用户认证请求(包括账号和密码等) * @return 认证成功的用户信息 */ @Override public UserDto authentication(AuthenticationRequest authenticationRequest) { // 校验参数是否为空 if (authenticationRequest == null || StringUtils.isEmpty(authenticationRequest.getUsername()) || StringUtils.isEmpty(authenticationRequest.getPassword())){ throw new RuntimeException("账号或密码为空"); } // 模拟用户查询 UserDto user = getUserDto(authenticationRequest.getUsername()); // 判断用户是否为空 if (user == null){ throw new RuntimeException("查询不到该用户"); } // 校验密码 if (!authenticationRequest.getPassword().equals(user.getPassword())){ throw new RuntimeException("账号或密码错误"); } // 认证成功返回用户信息 return user; } /** * 根据用户名查询用户信息(一般不放在这里) * @param username 用户名 * @return */ public UserDto getUserDto(String username){ return userMap.get(username); } // 模拟用户信息 private Map<String,UserDto> userMap = new HashMap<>(); { userMap.put("zhangsan", new UserDto("100001", "zhangsan", "123", "张三", "88880001")); userMap.put("lisi", new UserDto("100002", "lisi", "456", "李四", "88880002")); } }登录Controller,对/login请求处理,它调用AuthenticationService完成认证并返回登录结果提示信息
@RestController @RequestMapping public class LoginController { @Autowired private AuthenticationService authenticationService; /** * 登录 * @param authenticationRequest * @param session * @return */ @PostMapping(value = "/login", produces = {"text/plain;charset=utf-8"}) public String login(AuthenticationRequest authenticationRequest, HttpSession session){ UserDto userDto = authenticationService.authentication(authenticationRequest); return userDto.getUsername()+"登录成功"; } }
到目前为止最基础的认证功能已经完成,它仅仅实现了对用户身份凭证的校验,若某用 户认证成功,只能说明他是该系统的一个合法用户,仅此而已。
# 实现会话功能 🔥
会话是指用户登入系统后,系统会记住该用户的登录状态,他可以在系统连续操作直到退出系统的过程。相对于 HTTP 的无状态协议来说。
认证的目的是对系统资源的保护,每次对资源的访问,系统必须得知道是谁在访问资源,才能对该请求进行合法性拦截。因此,在认证成功后,一般会把认证成功的用户信息放入Session中,在后续的请求中,系统能够从 Session 中获取到当前用户,用这样的方式来实现会话机制。
# 增加会话控制
首先在UserDto中定义一个SESSION_USER_KEY,作为Session中存放登录用户信息的key
public static final String SESSION_USER_KEY = "_user";
修改LoginController,认证成功后,将用户信息放入当前会话。并增加用户登出方法,登出时将session置为失效
@RestController
@RequestMapping
public class LoginController {
@Autowired
private AuthenticationService authenticationService;
/**
* 登录
* @param authenticationRequest
* @param session
* @return
*/
@PostMapping(value = "/login", produces = {"text/plain;charset=utf-8"})
public String login(AuthenticationRequest authenticationRequest, HttpSession session){
UserDto userDto = authenticationService.authentication(authenticationRequest);
// 登录成功存入 session
session.setAttribute(UserDto.SESSION_USER_KEY, userDto);
return userDto.getUsername()+"登录成功";
}
/**
* 退出
* @param session
* @return
*/
@GetMapping(value = "/logout", produces = "text/plain;charset=utf-8")
public String logout(HttpSession session){
// 使会话无效
session.invalidate();
return "退出成功";
}
}
# 增加测试资源
/**
* 访问资源r1
* @param session
* @return
*/
@GetMapping(value = "/r/r1", produces = "text/plain;charset=utf-8")
public String accessR1(HttpSession session){
String fullName;
Object object = session.getAttribute(UserDto.SESSION_USER_KEY);
if (object == null){
fullName = "匿名";
} else {
UserDto userDto = (UserDto) object;
fullName = userDto.getFullName();
}
return fullName + "访问资源r1";
}
测试结果说明,在用户登录成功时,该用户信息已被成功放入 session,并且后续请求可以正常从 session 中获取当前登录用户信息,符合预期结果。
实际中不会在每个 Controller 中写,会在拦截器中统一拦截,见下一章节。
# 实现授权功能 🔥
现在我们已经完成了用户身份凭证的校验以及登录的状态保持,并且我们也知道了如何获取当前登录用户(从 Session中获取)的信息,接下来,用户访问系统需要经过授权,即需要完成如下功能:
- 匿名用户(未登录用户)访问拦截:禁止匿名用户访问某些资源
- 登录用户访问拦截:根据用户的权限决定是否能访问某些资源
步骤如下:
# 增加权限数据
在UserDto里增加权限属性,用于表示该登录用户所拥有的权限,同时修改 UserDto 的构造方法
/**
* 用户身份信息
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserDto {
public static final String SESSION_USER_KEY = "_user";
private String id;
private String username;
private String password;
private String fullName;
private String mobile;
// 用户权限
private Set<String> authorities;
}
并在 AuthenticationServiceImpl 中为模拟用户初始化权限,其中张三给了p1权限,李四给了p2权限。
// 模拟用户信息
private Map<String,UserDto> userMap = new HashMap<>();
{
HashSet<String> authorities1 = new HashSet<>();
authorities1.add("p1");// permission 1,令其和 r/r1 绑定
HashSet<String> authorities2 = new HashSet<>();
authorities2.add("p2");// permission 2,令其和 r/r2 绑定
userMap.put("zhangsan", new UserDto("100001", "zhangsan", "123", "张三", "88880001", authorities1));
userMap.put("lisi", new UserDto("100002", "lisi", "456", "李四", "88880002", authorities2));
}
# 增加测试资源
实现针对不同的用户能访问不同的资源,前提是得有多个资源,因此在LoginController中增加测试资源2
/**
* 访问资源r2
* @param session
* @return
*/
@GetMapping(value = "/r/r2", produces = "text/plain;charset=utf-8")
public String accessR2(HttpSession session){
String fullName;
Object object = session.getAttribute(UserDto.SESSION_USER_KEY);
if (object == null){
fullName = "匿名";
} else {
UserDto userDto = (UserDto) object;
fullName = userDto.getFullName();
}
return fullName + "访问资源r2";
}
# 实现授权拦截器
在interceptor包下定义SimpleAuthenticationInterceptor拦截器,实现授权拦截:
校验用户是否登录
校验用户是否拥有操作权限
@Component
public class SimpleAuthenticationInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 校验用户请求的 url 是否在用户的权限内
HttpSession session = request.getSession();
Object object = session.getAttribute(UserDto.SESSION_USER_KEY);
if (object == null){
// 未登录
writeContent(response, "请先登录");
return false;
}
UserDto userDto = (UserDto) object;
// 请求的 url(requestURI 包括查询参数)
String requestURI = request.getRequestURI();
if (requestURI.contains("/r/r1") && userDto.getAuthorities().contains("p1")) {
return true;
}
if (requestURI.contains("/r/r2") && userDto.getAuthorities().contains("p2")){
return true;
}
writeContent(response, "没有权限,拒绝访问");
return false;
}
private void writeContent(HttpServletResponse response, String content) throws IOException {
response.setContentType("text/html;charset=utf-8");
PrintWriter writer = response.getWriter();
writer.write(content);
writer.close();
}
}
在 WebConfig 中配置拦截器
/**
* 配置拦截器
*
* @param registry
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry
.addInterceptor(simpleAuthenticationInterceptor)
.excludePathPatterns("/", "/login", "/logout") // 放行登录等资源
.addPathPatterns("/r/**");// 其实这行写了后就不用再放行上面那些资源了
}
← 入门 Session—2 Security →